Youtube Videos:

* https://www.youtube.com/watch?v=rq8cL2XMM5M&index=3&list=PL-osiE80TeTsqhIuOqKhwlXsIBIdSeYtc
* https://www.youtube.com/watch?v=RSl87lqOXDE&index=4&list=PL-osiE80TeTsqhIuOqKhwlXsIBIdSeYtc

Online References:

* https://jeffknupp.com/blog/2017/03/27/improve-your-python-python-classes-and-object-oriented-programming/
* https://dbader.org/blog/abstract-base-classes-in-python

In [14]:
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable 
    def __init__(self, fname, lname):
        self.fname = fname
        self.lname = lname
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)

emp1 = Employee('Sri', 'Paladugu')
emp2 = Employee('Dhruv', 'Paladugu')

print( emp1.get_fullname() )
print( Employee.emp_count  )

# Trobule ensues when you treat class variables as instance attribute. 
# What the interpreter does in this case is, it creates an instance attribute with the same name and assigns to it.
# The class variable still remains intact with old value.
emp1.company = 'Verily'
print(emp1.company)
print(emp1.get_company())

print(emp2.company)
print(emp2.email)


Sri Paladugu
2
Verily
Company Name is: Google
Google
Dhruv.Paladugu@Google.com

Class Methods


In [15]:
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable
    raise_amount = 1.04
    def __init__(self, fname, lname):
        self.fname = fname
        self.lname = lname
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)
    
    @classmethod
    def set_raise_amt(cls, amount):
        cls.raise_amount = amount

emp1 = Employee('Sri', 'Paladugu')
emp2 = Employee('Dhruv', 'Paladugu')

Employee.set_raise_amt(1.05)
print(Employee.raise_amount)
print(emp1.raise_amount)
print(emp2.raise_amount)


1.05
1.05
1.05

Class Methods can be used to create alternate constructors


In [16]:
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable
    raise_amount = 1.04
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)
    
    @classmethod
    def set_raise_amt(cls, amount):
        cls.raise_amount = amount
    
    @classmethod
    def from_string(cls, emp_str):
        fname, lname, salary = emp_str.split("-")
        return cls(fname, lname, salary)

new_emp = Employee.from_string("Pradeep-Koganti-10000")
print(new_emp.email)


Pradeep.Koganti@Google.com

Static Methods

  • Instance methods take self as the first argument
  • Class methods take cls as the first argument
  • Static methods don't take instance or class as their argument, we just pass the arguments we want to work with.

Static methods don't operate on instance or class.


In [17]:
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable
    raise_amount = 1.04
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)
    
    @classmethod
    def set_raise_amt(cls, amount):
        cls.raise_amount = amount
    
    @classmethod
    def from_string(cls, emp_str):
        fname, lname, salary = emp_str.split("-")
        return cls(fname, lname, salary)
    
    @staticmethod
    def is_workday(day):
        if day.weekday() == 5 or day.weekday() == 6:
            return False
        else:
            return True

import datetime
my_date = datetime.date(2016, 7, 10)

print(Employee.is_workday(my_date))


False

Inheritance - Creating subclasses


In [19]:
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable
    raise_amount = 1.04
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)
    
    def apply_raise(self):
        self.salary = self.salary * self.raise_amount

class Developer(Employee):
    pass

dev1 = Developer('Sri', 'Paladugu', 1000)
print(dev1.get_fullname())
print(help(Developer)) # This command prints the Method resolution order. 
# Indicating the order in which the interpreter is going to look for methods.


Sri Paladugu
Help on class Developer in module __main__:

class Developer(Employee)
 |  Method resolution order:
 |      Developer
 |      Employee
 |      builtins.object
 |  
 |  Methods inherited from Employee:
 |  
 |  __init__(self, fname, lname, salary)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  get_company(self)
 |  
 |  get_fullname(self)
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors inherited from Employee:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes inherited from Employee:
 |  
 |  company = 'Google'
 |  
 |  emp_count = 1
 |  
 |  raise_amount = 1.04

None

Now what if you want Developer's raise_amount to be 10%?


In [20]:
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable
    raise_amount = 1.04
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)

    def apply_raise(self):
        self.salary = self.salary * self.raise_amount    
    
class Developer(Employee):
    raise_amount = 1.10

dev1 = Developer('Sri', 'Paladugu', 1000)
dev1.apply_raise()
print(dev1.salary)


1100.0

Now what if we want the Developer class to have an extra attribute like prog_lang?


In [21]:
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable
    raise_amount = 1.04
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)

    def apply_raise(self):
        self.salary = self.salary * self.raise_amount    
    
class Developer(Employee):
    raise_amount = 1.10
    
    def __init__(self, fname, lname, salary, prog_lang):
        super().__init__(fname, lname, salary)
        # or you can also use the following syntax
        # Employee.__init__(self, fname, lname, salary)
        self.prog_lang = prog_lang

dev1 = Developer('Sri', 'Paladugu', 1000, 'Python')
print(dev1.get_fullname())
print(dev1.prog_lang)


Sri Paladugu
Python

In [40]:
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable
    raise_amount = 1.04
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)

    def apply_raise(self):
        self.salary = self.salary * self.raise_amount    
    
class Developer(Employee):
    raise_amount = 1.10
    
    def __init__(self, fname, lname, salary, prog_lang):
        super().__init__(fname, lname, salary)
        # or you can also use the following syntax
        # Employee.__init__(self, fname, lname, salary)
        self.prog_lang = prog_lang

class Manager(Employee):
    def __init__(self, fname, lname, salary, employees = None): # Use None as default not empty list []
        super().__init__(fname, lname, salary)
        if employees is None:
            self.employees = []
        else:
            self.employees = employees
    def add_employee(self, emp):
        if emp not in self.employees:
            self.employees.append(emp)
    def remove_employee(self, emp):
        if emp in self.employees:
            self.employees.remove(emp)
    def print_emps(self):
        for emp in self.employees:
            print('--->', emp.get_fullname())

dev_1 = Developer('Sri', 'Paladugu', 1000, 'Python')
dev_2 = Developer('Dhruv', 'Paladugu', 2000, 'Java')
mgr_1 = Manager('Sue', 'Smith', 9000, [dev_1])
print(mgr_1.email)
print(mgr_1.print_emps())
mgr_1.add_employee(dev_2)
print(mgr_1.print_emps())

print('Is dev_1 an instance of Developer: ', isinstance(dev_1, Developer))
print('Is dev_1 an instance of Employee: ', isinstance(dev_1, Employee))
print('Is Developer an Subclass of Developer: ', issubclass(Developer, Developer))
print('Is Developer an Subclass of Employee: ', issubclass(Developer, Employee))


Sue.Smith@Google.com
---> Sri Paladugu
None
---> Sri Paladugu
---> Dhruv Paladugu
None
Is dev_1 an instance of Developer:  True
Is dev_1 an instance of Employee:  True
Is Developer an Subclass of Developer:  True
Is Developer an Subclass of Employee:  True

Dunder methods:

  1. __repr__
  2. __str__

In [47]:
class Employee:
    company = 'Google'
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
    def __repr__(self): # For other developers
        return "Employee('{}','{}','{}')".format(self.fname, self.lname, self.salary)
    def __str__(self): # For end user
        return '{} - {}'.format(self.get_fullname(), self.email)
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)

emp1 = Employee('Sri', 'Paladugu', 5000)
print(emp1)
print(repr(emp1))


Sri Paladugu - Sri.Paladugu@Google.com
Employee('Sri','Paladugu','5000')
  1. __add__
  2. __len__

In [53]:
# if you do: 1 + 2 internally the interpreter calls the dunder method __add__
print(int.__add__(1,2))
# Similarly # if you do: [2,3] + [4,5] internally the interpreter calls the dunder method __add__
print(list.__add__([2,3],[4,5]))

print('Paladugu'.__len__()) # This is same as len('Paladugu')


3
[2, 3, 4, 5]
8

In [51]:
class Employee:
    company = 'Google'
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
    def __repr__(self): # For other developers
        return "Employee('{}','{}','{}')".format(self.fname, self.lname, self.salary)
    def __str__(self): # For end user
        return '{} - {}'.format(self.get_fullname(), self.email)
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    def __add__(self, other):
        return self.salary + other.salary
    def __len__(self):
        return len(self.get_fullname())

emp1 = Employee('Sri', 'Paladugu', 5000)
emp2 = Employee('Dhruv', 'Paladugu', 5000)

print(emp1 + emp2)
print(len(emp1))


10000
12

Property Decorators


In [56]:
class Employee:
    company = 'Google'
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary

    @property
    def email(self):
        return '{}.{}@{}.com'.format(self.fname, self.lname, self.company)

    @property
    def fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    @fullname.setter
    def fullname(self, name):
        first, last = name.split(' ')
        self.fname = first
        self.lname = last
    
    @fullname.deleter
    def fullname(self):
        print('Delete Name!')
        self.fname = None
        self.lname = None

emp1 = Employee('Sri', 'Paladugu', 5000)
print(emp1.email)
print(emp1.fullname)
emp1.fullname = 'Ramki Paladugu'
print(emp1.email)
del emp1.fullname
print(emp1.email)


Sri.Paladugu@Google.com
Sri Paladugu
Ramki.Paladugu@Google.com
Delete Name!
None.None@Google.com

Abstract Base Classes in Python

What are Abstract Base Classes good for? A while ago I had a discussion about which pattern to use for implementing a maintainable class hierarchy in Python. More specifically, the goal was to define a simple class hierarchy for a service backend in the most programmer-friendly and maintainable way.

There was a BaseService that defines a common interface and several concrete implementations that do different things but all provide the same interface (MockService, RealService, and so on). To make this relationship explicit the concrete implementations all subclass BaseService.

To be as maintainable and programmer-friendly as possible the idea was to make sure that:

  • instantiating the base class is impossible; and
  • forgetting to implement interface methods in one of the subclasses raises an error as early as possible.

In [4]:
from abc import ABCMeta, abstractmethod

class Base(metaclass=ABCMeta):
    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

class Concrete(Base):
    def foo(self):
        pass

    # We forget to declare bar()

c = Concrete()


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-4-c65cfdf3ba42> in <module>()
     16     # We forget to declare bar()
     17 
---> 18 c = Concrete()

TypeError: Can't instantiate abstract class Concrete with abstract methods bar